﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;

namespace Sencha.Serialization.Bson
{
	[DebuggerDisplay("BsonWriter: written={BytesWritten}")]
	public class BsonStreamWriter : IBsonWriter
	{
		private ISerializationContext m_context;
		private Stream m_stream;
		private MemoryStream m_writeBuffer;
		private BinaryWriter m_writer;
		private BsonTokenInfo m_tokenInfo;
		private long m_bytesWriten;
		private bool m_pendingFlush;

		private IEnumerator<bool> m_writeMachine;

		[DebuggerDisplay("BsonToken: {Token}, value={Value}")]
		private class BsonTokenInfo
		{
			private BsonStreamWriter m_writer;
			private object m_value;
			private Dictionary<Type, IStrongBox> m_valueContainers;

			public BsonToken Token;
			public object Value
			{
				get
				{
					if (m_value is IStrongBox)
						return (m_value as IStrongBox).AsObject;
					else
						return m_value;
				}
				set
				{
					m_value = value;
				}
			}
			public Type ValueType
			{
				get
				{
					if (m_value is IStrongBox)
						return (m_value as IStrongBox).ValueType;
					else if (m_value != null)
						return m_value.GetType();
					else
						return null;
				}
			}

			public BsonTokenInfo(BsonStreamWriter writer)
			{
				if (writer == null)
					throw new ArgumentNullException("writer");

				m_valueContainers = new Dictionary<Type, IStrongBox>(32);
				m_writer = writer;
			}

			public T GetValue<T>()
			{
				if (m_value is IStrongBox)
					return (m_value as IStrongBox).Load<T>();
				else
					return (T)m_value;
			}
#if NO_TYPE_REFS
			private static readonly object SetTypedValueCallSite = new object();
			private delegate void SetStore<T>(IStrongBox @this, T value) where T : struct;

			public void SetTypedValue<T>(T value)
			{
				var valueType = typeof(T);
				if (valueType.IsValueType)
				{
					var container = default(IStrongBox);
					if (!m_valueContainers.TryGetValue(valueType, out container))
					{
						container = (IStrongBox)Activator.CreateInstance(typeof(StrongBox<>).MakeGenericType(valueType));
						m_valueContainers[valueType] = container;
					}

					container.Store(value);
										
					this.Value = container;
				}
				else
					this.Value = (object)value;
			}
#else
			public void SetTypedValue(TypedReference value)
			{
				var valueType = __reftype(value);
				if (valueType.IsValueType)
				{
					var container = default(IStrongBox);
					if (!m_valueContainers.TryGetValue(valueType, out container))
					{
						container = (IStrongBox)Activator.CreateInstance(typeof(StrongBox<>).MakeGenericType(valueType));
						m_valueContainers[valueType] = container;
					}
					container.Store(value);
					this.Value = container;
				}
				else
					this.Value = TypedReference.ToObject(value);
			}
#endif

			public Int64 EnsureInt64()
			{
				if (m_value == null)
					throw new ArgumentNullException("value");

				if (m_value is IStrongBox)
					return (m_value as IStrongBox).AsInt64;
				else if (m_value is Int64)
					return (long)m_value;
				else if (m_value is Int64)
					return (long)(ulong)m_value;
				else if (m_value is Int32)
					return (long)(int)m_value;
				else if (m_value is UInt32)
					return (long)(uint)m_value;
				else if (m_value is Int16)
					return (long)(short)m_value;
				else if (m_value is UInt16)
					return (long)(ushort)m_value;
				else if (m_value is byte)
					return (long)(byte)m_value;
				else if (m_value is sbyte)
					return (long)(sbyte)m_value;

				throw new ArgumentTypeMismatchException("m_value", m_value.GetType(), typeof(long), typeof(int), typeof(short), typeof(byte));
			}
			public int EnsureInt32()
			{
				if (m_value == null)
					throw new ArgumentNullException("value");

				if (m_value is IStrongBox)
					return (m_value as IStrongBox).AsInt32;
				else if (m_value is Int32)
					return (int)m_value;
				else if (m_value is UInt32)
					return (int)(uint)m_value;
				else if (m_value is Int16)
					return (int)(short)m_value;
				else if (m_value is UInt16)
					return (int)(ushort)m_value;
				else if (m_value is byte)
					return (int)(byte)m_value;
				else if (m_value is sbyte)
					return (int)(sbyte)m_value;

				throw new ArgumentTypeMismatchException("value", m_value.GetType(), typeof(int), typeof(short), typeof(byte));
			}
			public double EnsureDouble()
			{
				if (m_value == null)
					throw new ArgumentNullException("value");

				if (m_value is IStrongBox)
					return (m_value as IStrongBox).AsDouble;
				else if (m_value is Double)
					return (double)m_value;
				else if (m_value is Single)
					return (double)(float)m_value;
				else if (m_value is Decimal)
					return (double)(decimal)m_value;
				else if (m_value is Int32)
					return (int)m_value;
				else if (m_value is UInt32)
					return (int)(uint)m_value;
				else if (m_value is Int16)
					return (int)(short)m_value;
				else if (m_value is UInt16)
					return (int)(ushort)m_value;
				else if (m_value is byte)
					return (int)(byte)m_value;
				else if (m_value is sbyte)
					return (int)(sbyte)m_value;

				throw new ArgumentTypeMismatchException("value", m_value.GetType(), typeof(double), typeof(float), typeof(decimal), typeof(int), typeof(short), typeof(byte));
			}
			public BsonDateTime EnsureDateTime()
			{
				if (m_value == null)
					throw new ArgumentNullException("value");

				if (m_value is DateTime)
					return (DateTime)m_value;
				if (m_value is BsonDateTime)
					return (BsonDateTime)m_value;
				else if (m_value is DateTimeOffset)
					return ((DateTimeOffset)m_value).DateTime;
				else if (m_value is StrongBox<DateTime>)
					return (m_value as StrongBox<DateTime>).Value;
				else if (m_value is StrongBox<DateTimeOffset>)
					return (m_value as StrongBox<DateTimeOffset>).Value.DateTime;
				else if (m_value is StrongBox<BsonDateTime>)
					return (m_value as StrongBox<BsonDateTime>).Value;
				else
					throw new ArgumentTypeMismatchException("value", m_value.GetType(), typeof(DateTime), typeof(DateTimeOffset));
			}
			public T EnsureExact<T>() where T : struct
			{
				if (m_value == null)
					throw new ArgumentNullException("value");


				if (!(m_value is T) && !(m_value is StrongBox<T>))
					throw new ArgumentTypeMismatchException("value", m_value.GetType(), typeof(T));

				if (m_value is StrongBox<T>)
					return (m_value as StrongBox<T>).Value;
				else
					return (T)m_value;
			}
			public T EnsureExactRef<T>() where T : class
			{
				if (m_value == null)
					throw new ArgumentNullException("value");


				if (!(m_value is T))
					throw new ArgumentTypeMismatchException("value", m_value.GetType(), typeof(T));

				return (T)m_value;
			}
			public void EnsureNoValue()
			{
				if (m_value != null)
					throw new ArgumentException(BsonLoc.GetErrorString("Exception_ShouldNotProvideValue"), "value");
			}
		}
		[DebuggerDisplay("Document: {Type}, pos={Position}")]
		private struct Document
		{
			public long Position;
			public BsonToken Type;
			public BsonToken ClosingToken;

			public Document(BsonToken docType, long position)
			{
				this.Type = docType;
				this.ClosingToken = docType == BsonToken.BeginDocument ? BsonToken.EndOfDocument :
									docType == BsonToken.BeginArray ? BsonToken.EndOfArray :
									docType == BsonToken.JavaScriptWithScope ? BsonToken.EndOfDocument :
									BsonToken.None;
				this.Position = position;
			}

			public override string ToString()
			{
				return string.Format("{0}", this.Type);
			}
		}

		public bool FastWrite { get; set; }
		public long BytesWritten { get { return m_bytesWriten; } }

		public BsonStreamWriter(Stream stream, ISerializationContext context)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			if (context == null)
				throw new ArgumentNullException("context");

			m_tokenInfo = new BsonTokenInfo(this);
			m_stream = stream;
			m_context = context;
		}


		public void Write<T>(BsonToken token, T value)
		{
			m_tokenInfo.Token = token;
#if NO_TYPE_REFS
			m_tokenInfo.SetTypedValue(value);
#else
			m_tokenInfo.SetTypedValue(__makeref(value));
#endif

			this.WriteNextValue();
		}
		public void WriteBson(byte[] bsonData, int index, int count)
		{
			if (bsonData == null)
				throw new ArgumentNullException("bsonData");

			this.PrepareWriter();

			m_writer.Write(bsonData, index, count);
		}
		public void Flush()
		{
			if (m_writer != null)
				m_writer.Flush();

			if (m_writeBuffer != null)
			{
				// we can flush buffer only after write (state-)machine is finished writing
				if (m_writeMachine != null)
				{
					m_pendingFlush = true;
					return;
				}

				m_writeBuffer.WriteTo(m_stream);
				m_writeBuffer.Position = 0;
				m_writeBuffer.SetLength(0);
			}
		}
		public void Reset()
		{
			m_bytesWriten = 0;
			m_writeMachine = null;
			m_tokenInfo = new BsonTokenInfo(this);
			m_pendingFlush = false;

			if (m_writeBuffer != null)
			{
				m_writeBuffer.Position = 0;
				m_writeBuffer.SetLength(0);
			}
			if (m_writer != null)
				m_writer.Flush();
		}

		private void PrepareWriter()
		{
			if (m_writer != null)
				return;

			if (m_stream.CanSeek || !this.FastWrite)
				m_writer = new BinaryWriter(m_stream);
			else
				m_writer = new BinaryWriter(m_writeBuffer = new MemoryStream());
		}
		private void WriteNextValue()
		{
			this.PrepareWriter();

			if (m_writeMachine == null)
				m_writeMachine = this.WriteToken(m_tokenInfo).GetEnumerator();

			// write token, if result is true then top level object was closed
			var eoo = m_writeMachine.MoveNext();
			if (!eoo) // so we dispose this state machine and unlock Flush
				m_writeMachine = null;
		}
		private IEnumerable<bool> WriteToken(BsonTokenInfo info)
		{
			var stack = new Stack<Document>(10);
			var position = 0;
			var member = (string)null;
			var arrayLength = 0;

		start:

			if (stack.Count > 0)
			{
				// write type and name				
				if (IsInsideObject(stack) && (info.Token != BsonToken.Member && info.Token != BsonToken.EndOfDocument))
				{
					if (member == null)
						throw new System.Runtime.Serialization.SerializationException("You should provide member name prior to calling Write(value).");

					m_writer.Write((byte)info.Token); position += 1; m_bytesWriten += 1;
					this.WriteCString(m_writer, member, ref position);
					member = null;
				}
				else if (IsInsideArray(stack) && info.Token != BsonToken.EndOfArray)
				{
					m_writer.Write((byte)info.Token); position += 1; m_bytesWriten += 1;

					if (this.FastWrite)
						this.WriteCString(m_writer, "", ref position);
					else
						this.WriteCString(m_writer, arrayLength.ToString(), ref position);

					arrayLength++;
				}
			}

			// write value
			switch (info.Token)
			{
				case BsonToken.BeginArray:
				case BsonToken.BeginDocument:
					stack.Push(new Document(info.Token, position));
					m_writer.Write((int)0); position += 4; m_bytesWriten += 4; // length placeholder
					info.EnsureNoValue();
					break;
				case BsonToken.EndOfArray:
				case BsonToken.EndOfDocument:
					var doc = stack.Peek();
					if (doc.ClosingToken != info.Token)
						throw new System.Runtime.Serialization.SerializationException(string.Format("Unexpected block closing. Expected '{0}' got '{1}'.", doc.ClosingToken, info.Token));
					stack.Pop();
					if (doc.Type != BsonToken.JavaScriptWithScope)
					{
						m_writer.Write((byte)0); position += 1; m_bytesWriten += 1; // end of document/array
					}
					arrayLength = 0;
					var length = position - (int)doc.Position;
					if (!this.FastWrite)
					{
						m_writer.Flush();
						this.WriteLength(m_writer, length);
					}
					break;
				case BsonToken.Binary:
					var subType = (byte)0;
					var binary = (byte[])null;
					var startIndex = 0;
					var len = 0;
					if (info.Value is BsonBinary)
					{
						var bsonBinary = (info.Value as BsonBinary);
						binary = bsonBinary.Bytes;
						subType = bsonBinary.SubType;
						startIndex = bsonBinary.Offset;
						len = bsonBinary.Count;
					}
					else
					{
						binary = info.EnsureExactRef<byte[]>();
						startIndex = 0;
						len = binary.Length;
					}

					var binaryLength = len + (subType == 2 ? 4 : 0);
					m_writer.Write((int)binaryLength); position += 4; m_bytesWriten += 4;
					m_writer.Write((byte)subType); position += 1; m_bytesWriten += 1;
					if (subType == 2)
						m_writer.Write((int)binary.Length);
					m_writer.Write(binary, startIndex, len); position += binaryLength; m_bytesWriten += binaryLength;
					break;
				case BsonToken.Boolean:
					var boolean = info.EnsureExact<bool>();
					m_writer.Write((byte)(boolean ? 1 : 0)); position += 1; m_bytesWriten += 1;
					break;
				case BsonToken.DateTime:
					var datetime = info.EnsureDateTime();
					m_writer.Write((long)datetime.Raw); position += 8; m_bytesWriten += 8;
					break;
				case BsonToken.Double:
					var doubleValue = info.EnsureDouble();
					m_writer.Write((double)doubleValue); position += 8; m_bytesWriten += 8;
					break;
				case BsonToken.Int32:
					var int32 = info.EnsureInt32();
					m_writer.Write((int)int32); position += 4; m_bytesWriten += 4;
					break;
				case BsonToken.Timestamp:
					var timestamp = info.EnsureExact<BsonTimestamp>();
					m_writer.Write((long)timestamp.Raw); position += 8; m_bytesWriten += 8;
					break;
				case BsonToken.Int64:
					var int64 = info.EnsureInt64();
					m_writer.Write((long)int64); position += 8; m_bytesWriten += 8;
					break;
				case BsonToken.Member:
					member = this.CheckCString(info.EnsureExactRef<string>());
					break;
				case BsonToken.MaxKey:
				case BsonToken.MinKey:
				case BsonToken.Undefined:
				case BsonToken.Null:
					info.EnsureNoValue();
					break;
				case BsonToken.ObjectId:
				case BsonToken.Reference:
					var oid = info.EnsureExact<BsonObjectId>();
					m_writer.Write((int)oid.timeStamp);
					m_writer.Write((int)oid.machineAndPid);
					m_writer.Write((int)oid.pidAndIncrement); position += 12; m_bytesWriten += 12;
					break;
				case BsonToken.Regex:
					var regex = default(BsonRegex);
					if (info.Value is System.Text.RegularExpressions.Regex)
						regex = new BsonRegex(info.Value as System.Text.RegularExpressions.Regex);
					else
						regex = info.EnsureExactRef<BsonRegex>();

					var pattern = this.CheckCString(regex.Pattern);
					var options = this.CheckCString(regex.Options);
					this.WriteCString(m_writer, pattern, ref position);
					this.WriteCString(m_writer, options, ref position);
					break;
				case BsonToken.JavaScriptWithScope:
					var jsScopedCode = info.EnsureExactRef<BsonJavaScriptWithScope>();
					stack.Push(new Document(BsonToken.JavaScriptWithScope, position));
					m_writer.Write((int)0); position += 4; m_bytesWriten += 4; // length placeholder
					this.WriteString(m_writer, jsScopedCode.Code, ref position);
					break;
				case BsonToken.JavaScript:
					var code = default(string);
					if (info.Value is string)
						code = (string)info.Value;
					else
						code = info.EnsureExactRef<BsonJavaScript>().Code;
					this.WriteString(m_writer, code, ref position);
					break;
				case BsonToken.String:
					var str = info.EnsureExactRef<string>();
					this.WriteString(m_writer, str, ref position);
					break;
				case BsonToken.Symbol:
					var symbol = default(string);
					if (info.Value is string)
						symbol = (string)info.Value;
					else
						symbol = info.EnsureExactRef<BsonSymbol>().Raw;

					this.WriteString(m_writer, symbol, ref position);
					break;
				default:
					throw new System.Runtime.Serialization.SerializationException(string.Format("Attemt to write wrong or unsupported token '{0}' in BSON stream", info.Token));
			}

			if (stack.Count == 0)
				yield break;

			yield return true;
			goto start;
		}
		private void WriteLength(BinaryWriter writer, int length)
		{
			m_writer.BaseStream.Seek(-length, SeekOrigin.Current);
			m_writer.BaseStream.WriteByte((byte)length);
			m_writer.BaseStream.WriteByte((byte)(length >> 8));
			m_writer.BaseStream.WriteByte((byte)(length >> 0x10));
			m_writer.BaseStream.WriteByte((byte)(length >> 0x18));
			m_writer.BaseStream.Seek(length - 4, SeekOrigin.Current);
		}
		private void WriteString(BinaryWriter writer, string value, ref int position)
		{
			writer.Write((int)value.Length + 1); position += 4; m_bytesWriten += 4;
			if (value.Length > 0)
			{
				var valueBytes = this.Context.Encoding.GetBytes(value);
				writer.Write(valueBytes); position += valueBytes.Length; m_bytesWriten += valueBytes.Length;
			}
			writer.Write((byte)0); position += 1; m_bytesWriten += 1;
		}
		private void WriteCString(BinaryWriter writer, string value, ref int position)
		{
			if (value.Length > 0)
			{
				var valueBytes = this.Context.Encoding.GetBytes(value);
				writer.Write(valueBytes); position += valueBytes.Length; m_bytesWriten += valueBytes.Length;
			}
			writer.Write((byte)0); position += 1; m_bytesWriten += 1;
		}
		private string CheckCString(string value)
		{
			if (value == null)
				throw new ArgumentNullException("value");

			if (value.IndexOf('\0') != -1)
				throw new ArgumentException(BsonLoc.GetErrorString("Exception_InvalidCStringValue"));

			return value;
		}

		private bool IsInsideObject(Stack<Document> stack)
		{
			if (stack.Count == 0)
				return false;
			return stack.Peek().Type == BsonToken.BeginDocument;
		}
		private bool IsInsideArray(Stack<Document> stack)
		{
			if (stack.Count == 0)
				return false;
			return stack.Peek().Type == BsonToken.BeginArray;
		}

		#region IJsonWriter Members

		public ISerializationContext Context
		{
			get { return m_context; }
		}

		long IJsonWriter.CharactersWritten { get { return m_bytesWriten; } }

		void IJsonWriter.Write(string value)
		{
			this.Write(BsonToken.String, value);
		}
		void IJsonWriter.Write(JsonMember value)
		{
			this.Write(BsonToken.Member, value.ToString());
		}
		void IJsonWriter.Write(int number)
		{
			this.Write(BsonToken.Int32, number);
		}
		void IJsonWriter.Write(uint number)
		{
			this.Write(BsonToken.Int32, (int)number);
		}
		void IJsonWriter.Write(long number)
		{
			this.Write(BsonToken.Int64, number);
		}
		void IJsonWriter.Write(ulong number)
		{
			this.Write(BsonToken.Int64, (long)number);
		}
		void IJsonWriter.Write(float number)
		{
			this.Write(BsonToken.Double, (double)number);
		}
		void IJsonWriter.Write(double number)
		{
			this.Write(BsonToken.Double, (double)number);
		}
		void IJsonWriter.Write(decimal number)
		{
			this.Write(BsonToken.Double, (double)number);
		}
		void IJsonWriter.Write(bool value)
		{
			this.Write(BsonToken.Boolean, value);
		}
		void IJsonWriter.Write(DateTime datetime)
		{
			this.Write(BsonToken.DateTime, datetime);
		}
		void IJsonWriter.Write(JsonToken token)
		{
			switch (token)
			{
				case JsonToken.BeginArray: this.Write(BsonToken.BeginArray, (object)null); break;
				case JsonToken.EndOfArray: this.Write(BsonToken.EndOfArray, (object)null); break;
				case JsonToken.BeginObject: this.Write(BsonToken.BeginDocument, (object)null); break;
				case JsonToken.EndOfObject: this.Write(BsonToken.EndOfDocument, (object)null); break;
				case JsonToken.Null: this.Write(BsonToken.Null, (object)null); break;
				default:
					throw new Sencha.Serialization.Exceptions.UnserializableJsonToken(token, this);
			}
		}
		void IJsonWriter.WriteJson(string jsonString)
		{
			if (jsonString == null)
				throw new ArgumentNullException("jsonString");

			throw new NotSupportedException();
		}
		void IJsonWriter.WriteJson(char[] jsonString, int index, int charCount)
		{
			if (jsonString == null)
				throw new ArgumentNullException("jsonString");

			throw new NotSupportedException();
		}
		#endregion
	}
}
